/**
 * 音频采集：获取麦克风 -> Float32 PCM -> 重采样到 16k -> Int16 分片回调
 * 为便于快速联调，这里用 ScriptProcessorNode 实现（浏览器支持广，延迟适中）。
 */

export type AudioChunkHandler = (chunk: ArrayBuffer) => void

export interface AudioCaptureOptions {
  targetSampleRate?: number // 默认 16000
  chunkMs?: number // 期望输出分片时长，默认 200ms
}

export class AudioCapturer {
  private ctx?: AudioContext
  private source?: MediaStreamAudioSourceNode
  private processor?: ScriptProcessorNode
  private stream?: MediaStream

  private targetRate: number
  private chunkMs: number

  private resampledBuffer: Float32Array = new Float32Array(0)
  private isRunning = false

  constructor(opts: AudioCaptureOptions = {}) {
    this.targetRate = opts.targetSampleRate ?? 16000
    this.chunkMs = opts.chunkMs ?? 200
  }

  async start(onChunk: AudioChunkHandler) {
    if (this.isRunning) return
    this.isRunning = true
    this.stream = await navigator.mediaDevices.getUserMedia({ audio: true })
    this.ctx = new (window.AudioContext || (window as any).webkitAudioContext)()
    this.source = this.ctx.createMediaStreamSource(this.stream)

    const bufferSize = 4096
    this.processor = this.ctx.createScriptProcessor(bufferSize, 1, 1)

    const inputRate = this.ctx.sampleRate // 例如 48000
    const frameSamples = Math.round((this.targetRate * this.chunkMs) / 1000) // 200ms -> 3200 samples

    this.processor.onaudioprocess = (ev) => {
      if (!this.isRunning) return
      const input = ev.inputBuffer.getChannelData(0) // Float32
      // 重采样到目标采样率
      const resampled = resampleLinear(input, inputRate, this.targetRate)
      this.resampledBuffer = concatFloat32(this.resampledBuffer, resampled)

      // 足量则输出若干整块
      while (this.resampledBuffer.length >= frameSamples) {
        const block = this.resampledBuffer.slice(0, frameSamples)
        this.resampledBuffer = this.resampledBuffer.slice(frameSamples)
        const int16 = float32ToInt16(block)
        onChunk(int16.buffer)
      }
    }

    this.source.connect(this.processor)
    this.processor.connect(this.ctx.destination)
  }

  stop() {
    this.isRunning = false
    try { this.processor?.disconnect() } catch {}
    try { this.source?.disconnect() } catch {}
    try { this.ctx?.close() } catch {}
    try { this.stream?.getTracks().forEach((t) => t.stop()) } catch {}
    this.processor = undefined
    this.source = undefined
    this.ctx = undefined
    this.stream = undefined
    this.resampledBuffer = new Float32Array(0)
  }
}

function concatFloat32(a: Float32Array, b: Float32Array) {
  const out = new Float32Array(a.length + b.length)
  out.set(a, 0)
  out.set(b, a.length)
  return out
}

function resampleLinear(input: Float32Array, fromRate: number, toRate: number) {
  if (fromRate === toRate) return input
  const ratio = fromRate / toRate
  const outLen = Math.round(input.length / ratio)
  const output = new Float32Array(outLen)
  let pos = 0
  for (let i = 0; i < outLen; i++) {
    const idx = i * ratio
    const i0 = Math.floor(idx)
    const i1 = Math.min(i0 + 1, input.length - 1)
    const frac = idx - i0
    const s = input[i0] * (1 - frac) + input[i1] * frac
    output[pos++] = s
  }
  return output
}

function float32ToInt16(f32: Float32Array) {
  const out = new Int16Array(f32.length)
  for (let i = 0; i < f32.length; i++) {
    let s = Math.max(-1, Math.min(1, f32[i]))
    out[i] = s < 0 ? s * 0x8000 : s * 0x7fff
  }
  return out
}

export default AudioCapturer


